package cn.quevo.beetlsql.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.beetl.sql.core.ClasspathLoader.SQLFileVersion;
import org.beetl.sql.core.DefaultSQLIdNameConversion;
import org.beetl.sql.core.SQLIdNameConversion;
import org.beetl.sql.core.SQLLoader;
import org.beetl.sql.core.SQLSource;
import org.beetl.sql.core.SQLTableSource;
import org.beetl.sql.core.db.DBStyle;
import org.beetl.sql.core.db.PostgresStyle;
import org.beetl.sql.core.kit.MDParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 自定义Beetlsql的SQLLoader(重写org.beetl.sql.core.ClasspathLoader)
 *
 * @param null
 * @author July july_sky@foxmail.com
 * @version 1.0
 * @date 2018-03-09 18:24:49
 * @Copyright ©2015-2035 湘豫(北京)科技有限公司. All Rights Reserved.
 * @return
 */
public class BeetlClasspathLoader implements SQLLoader {

    private final static Logger log = LoggerFactory.getLogger(BeetlClasspathLoader.class);

    // SQL文件根目录(相对Classpath)
    protected String sqlRoot = null;
    // 换行符
    protected String lineSeparator = System.getProperty("line.separator", "\n");
    // 实际路径缓存
    protected Map<String, SQLSource> sqlSourceMap = new ConcurrentHashMap<String, SQLSource>();
    // 数据库方言
    protected DBStyle dbs = null;
    // 是否自动检测变化
    protected boolean autoCheck = true;
    // 字符编码
    protected String charset = null;
    // SQL ID转换器
    protected SQLIdNameConversion sqlIdNameConversion = new DefaultSQLIdNameConversion();

    protected SQLSource NO_EXIST = new SQLSource();

    public BeetlClasspathLoader() {
        this("/sql");
    }

    public BeetlClasspathLoader(String root) {
        this(root, new PostgresStyle());
    }

    public BeetlClasspathLoader(String root, DBStyle dbs) {
        this.sqlRoot = root;
        this.dbs = dbs;
    }

    @Override
    public SQLSource getSQL(String id) {
        return this.tryLoadSQL(id);
    }

    /**
     * 获取SQLSource
     *
     * @param id
     * @return
     * @author July july_sky@foxmail.com
     * @date 2017年11月30日 上午11:24:24
     */
    protected SQLSource tryLoadSQL(String id) {
        // 从缓存取得SQLSource
        SQLSource sqlSource = sqlSourceMap.get(id);
        if (sqlSource == null || sqlSource == this.NO_EXIST) {
            return this.getSQLSource(id);
        }
        boolean hasLoad = this.autoCheck && isModified(id);
        if (hasLoad) {
            return this.getSQLSource(id);
        }
        return sqlSource;

    }

    /**
     * 获取SQLSource
     *
     * @param id
     * @return
     * @author July july_sky@foxmail.com
     * @date 2017年11月30日 上午11:32:17
     */
    private SQLSource getSQLSource(String id) {
        if (loadSql(id)) {
            return sqlSourceMap.get(id);
        }
        sqlSourceMap.put(id, this.NO_EXIST);
        return null;
    }

    @Override
    public boolean isModified(String id) {
        SQLSource source = this.sqlSourceMap.get(id);
        if (source != null && source instanceof SQLTableSource && source == NO_EXIST) {
            return false;
        }
        // 先判断DB目录下有没有SQL文件，如果DB下游SQL文件并且有过修改，则直接返回true
        long oldDbVersion = source.getVersion().db;
        URL db = this.getSQLFile(id, false);
        if (getURLVersion(db) != oldDbVersion) {
            return true;
        }
        // 如果DB目录下没有SQL文件，或DB目录下的文件没有修改，则判断Root下有没有SQL文件，并验证有没有修改
        long oldRootVersion = source.getVersion().root;
        URL root = this.getSQLFile(id, true);
        if (getURLVersion(root) == oldRootVersion) {
            return true;
        }
        return false;
    }

    /**
     * 获取文件最后修改时间
     *
     * @param url
     * @return
     * @author July july_sky@foxmail.com
     * @date 2017年11月30日 上午11:15:09
     */
    protected static Long getURLVersion(URL url) {
        // 如果URL为空，或者非本地文件则直接返回0
        if (url == null || !url.getProtocol().equals("file")) {
            return 0l;
        }
        return new File(url.getFile()).lastModified();
    }

    /**
     * 验证文件是否存在
     */
    public boolean exist(String id) {
        return this.tryLoadSQL(id) != null;
    }

    @Override
    public void addSQL(String id, SQLSource source) {
        sqlSourceMap.put(id, source);

    }

    /***
     *  考虑到跨数据库支持，ClasspathLoader加载SQL顺序如下：
     首先根据DBStyle.getName() 找到对应的数据库名称，然后在ROOT/dbName 下找对应的sql，
     如果ROOT/dbName 文件目录不存在，或者相应的sql文件不存在，再搜索ROOT目录下的sql文件。
     如mysql 里查找user.select2,顺序如下：
     - 先找ROOT/mysql/user.sql 文件，如果有此文件，且包含了select2，则返回此sql语句，
     - 如果没有，下一步查找ROOT/mysql/user.md,如果有此文件，且包含了slect2，则返回sql语句
     - 如果没有，下一步查找ROOT/user.sql,如果有此文件，且包含了slect2，则返回sql语句
     - 如果没有，下一步查找ROOT/user.md,如果有此文件，且包含了slect2，则返回sql语句
     - 都没有，抛错，告诉用户未在ROOT/,或者ROOT/mysql 下找到相关sql
     *
     * @return
     */
    private boolean loadSql(String id) {
        // 读取db下的SQL文件
        URL url = this.getSQLFile(id, false);
        // 验证DB下是否存在此SQL，如果存在则返回true
        if (readSqlFile(id, url, false)) {
            return true;
        }
        url = this.getSQLFile(id, true);
        // 如果DB下不存在，则看root下是否存在
        return readSqlFile(id, url, true);
    }

    /**
     * 读取并验证SQL文件是否存在
     *
     * @param id
     * @param url
     * @param isRoot 是否根目录
     * @return
     * @author July july_sky@foxmail.com
     * @date 2017年11月30日 上午11:38:54
     */
    private boolean readSqlFile(String id, URL url, boolean isRoot) {
        if (url == null) {
            return false;
        }
        // 获取资源文件
        try (InputStream ins = url.openStream();) {
            if (ins == null) {
                return false;
            }
            String modelName = id.substring(0, id.lastIndexOf(".") + 1);
            long lastModified = getURLVersion(url);
            try (BufferedReader br = new BufferedReader(new InputStreamReader(ins, charset));) {
                MDParser parser = new MDParser(modelName, br);
                SQLSource source = null;
                SQLFileVersion version = null;
                while ((source = parser.next()) != null) {
                    version = new SQLFileVersion();
                    version.url = url;
                    if (isRoot) {
                        version.root = lastModified;
                    } else {
                        version.db = lastModified;
                    }
                    source.setVersion(version);
                    sqlSourceMap.put(source.getId(), source);
                }
            } catch (Exception e) {
                // 读取失败返回true
                log.error(e.getMessage(), e);
                return false;
            }
        } catch (Exception e) {
            // 文件不存在直接返回false
            log.error(e.getMessage(), e);
            return false;
        }
        return true;
    }

    public Map<String, SQLSource> getSqlSourceMap() {
        return sqlSourceMap;
    }

    public String getSqlRoot() {
        return sqlRoot;
    }

    public void setSqlRoot(String sqlRoot) {
        this.sqlRoot = sqlRoot;
    }

    /**
     * 获取.md/.sql文件 <br/>
     * 文件需放在classpath下
     *
     * @param id
     * @return
     * @author July july_sky@foxmail.com
     * @date 2017年11月30日 下午1:07:56
     */
    private URL getSQLFile(String id, boolean isRoot) {
        String path = this.sqlIdNameConversion.getPath(id);
        String file = sqlRoot + "/" + (isRoot ? "" : dbs.getName() + "/") + path + ".md";
        URL url = this.getFile(file);
        if (url == null) {
            file = sqlRoot + "/" + (isRoot ? "" : dbs.getName() + "/") + path + ".sql";
            url = this.getFile(file);
        }
        return url;
    }

    /**
     * 获取文件所在URL
     *
     * @param filePath
     * @return
     * @author July july_sky@foxmail.com
     * @date 2017年11月30日 下午1:15:51
     */
    private URL getFile(String filePath) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader != null) {
            URL url = loader.getResource(filePath);
            return url != null ? url : this.getClass().getResource(filePath);
        }
        return this.getClass().getResource(filePath);
    }

    @Override
    public boolean isAutoCheck() {
        return this.autoCheck;
    }

    @Override
    public void setAutoCheck(boolean check) {
        this.autoCheck = check;

    }

    public DBStyle getDbs() {
        return dbs;
    }

    public void setDbs(DBStyle dbs) {
        this.dbs = dbs;
    }

    @Override
    public String getCharset() {

        return charset;
    }

    @Override
    public void setCharset(String charset) {
        this.charset = charset;

    }

    public String toString() {
        return this.sqlRoot;
    }

    @Override
    public void setSQLIdNameConversion(SQLIdNameConversion sqlIdNc) {
        this.sqlIdNameConversion = sqlIdNc;

    }

    @Override
    public void setDbStyle(DBStyle dbStyle) {
        this.dbs = dbStyle;
    }
}
